package com.tianji.promotion.service.impl;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.domain.query.UserCouponQuery;
import com.tianji.promotion.domain.vo.CouponVO;
import com.tianji.promotion.enums.ExchangeCodeStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.promotion.utils.CodeUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.aop.framework.AopContext;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * <p>
 * 用户领取优惠券的记录，是真正使用的优惠券信息 服务实现类
 * 单体锁
 * </p>
 *
 * @author huanmo
 * @since 2025-09-27
 */
@RequiredArgsConstructor
public class UserCouponServiceImpl4 extends ServiceImpl<UserCouponMapper, UserCoupon> {

    private final CouponMapper couponMapper;

    private final IExchangeCodeService codeService;

    public void receiveCoupon(Long couponId) {
        // 1.查询优惠券
        Coupon coupon = couponMapper.selectById(couponId);
        if (coupon == null) {
            throw new BadRequestException("优惠券不存在");
        }
        // 2.校验发放时间
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
            throw new BadRequestException("优惠券发放已经结束或尚未开始");
        }
        // 3.校验库存
        if (coupon.getIssueNum() >= coupon.getTotalNum()) {
            throw new BadRequestException("优惠券库存不足");
        }
        Long userId = UserContext.getUser();
        // 4.校验并生成用户券
        synchronized (userId.toString().intern()) {
            // Spring事务自调用问题。当在同一个类中，一个非事务方法receiveCoupon()调用另一个带有@Transactional注解的方法checkAndCreateUserCoupon()时，事务注解不会生效
            // 解决方案：
            // 方案1：将事务方法移到另一个Service（推荐）
            // 方案2：使用编程式事务
            // 方案3：将事务注解放在外层方法receiveCoupon()
            // 使用方案：借助AspectJ - 事务失效的原因是方法内部调用走的是this，而不是代理对象。那我们只要想办法获取代理对象就可以了。
            IUserCouponService userCouponService = (IUserCouponService) AopContext.currentProxy();
            checkAndCreateUserCoupon(coupon, userId, null);
        }
    }

    public void exchangeCoupon(String code) {
        // 1.校验并解析兑换码
        long serialNum = CodeUtil.parseCode(code);
        // 2.校验是否已经兑换 SETBIT KEY 4 1 ，这里直接执行setBit，通过返回值来判断是否兑换过
        // Redis的SETBIT操作是原子的，提供了第一层快速防重
        boolean exchanged = codeService.updateExchangeMark(serialNum, true);
        if (exchanged) {
            throw new BizIllegalException("兑换码已经被兑换过了");
        }
        try {
            // 3.查询兑换码对应的优惠券id
            ExchangeCode exchangeCode = codeService.getById(serialNum);
            if (exchangeCode == null) {
                throw new BizIllegalException("兑换码不存在！");
            }
            // 4.是否过期
            LocalDateTime now = LocalDateTime.now();
            if (now.isAfter(exchangeCode.getExpiredTime())) {
                throw new BizIllegalException("兑换码已经过期");
            }
            // 5.校验并生成用户券
            // 5.1.查询优惠券
            Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
            // 5.2.查询用户
            Long userId = UserContext.getUser();
            // 5.3.校验并生成用户券，更新兑换码状态
            synchronized (userId.toString().intern()) {
                IUserCouponService userCouponService = (IUserCouponService) AopContext.currentProxy();
                checkAndCreateUserCoupon(coupon, userId, (int) serialNum);
            }
        } catch (Exception e) {
            // 重置兑换的标记 0
            codeService.updateExchangeMark(serialNum, false);
            // 异常必须被抛出，才能够回滚
            throw e;
        }
    }

    public PageDTO<CouponVO> queryMyCouponByPage(UserCouponQuery query) {
        Long userId = UserContext.getUser();
        // 1.查询我的优惠券列表
        Page<UserCoupon> page = lambdaQuery()
                .eq(UserCoupon::getUserId, userId)
                .eq(query.getStatus() != null, UserCoupon::getStatus, query.getStatus())
                .page(query.toMpPageDefaultSortByCreateTimeDesc());
        List<UserCoupon> records = page.getRecords();
        if (CollUtils.isEmpty(records)) {
            return PageDTO.empty(page);
        }
        // 2.查询优惠券
        List<Long> couponIds = records.stream()
                .map(UserCoupon::getCouponId)
                .distinct()
                .collect(Collectors.toList());
        List<Coupon> coupons = couponMapper.selectBatchIds(couponIds);
        if (CollUtils.isEmpty(coupons)) {
            return PageDTO.empty(page);
        }
        // 3.封装VO
        List<CouponVO> list = new ArrayList<>(records.size());
        for (Coupon coupon : coupons) {
            CouponVO vo = BeanUtils.copyProperties(coupon, CouponVO.class);
            list.add(vo);
        }
        return PageDTO.of(page, list);
    }

    /**
     * 为什么对 userId 上锁：
     * 目的：防止同一用户重复领取优惠券
     * 只对同一用户加锁，不同用户可以并行领取
     * <p>
     * 为什么使用 intern()：
     * 返回字符串在字符串常量池中的引用
     * 保证相同内容的字符串返回同一个对象引用
     * 保证锁对象的唯一性
     * userId.toString()每调用一次，就会生成一个新的String()，同时会生成一把新的锁
     * <p>
     * 在事务和锁并行存在时，一定要考虑事务和锁的边界问题。由于事务的隔离级别问题，可能会导致不同事务之间数据不可见，往往会产生一些不可预期的现象。
     * 当线程1的锁已经释放，但事务未提交时，数据库的数据不会更新。因此线程2可以趁机获取锁，然后更新数据。最终两个线程的事务都提交之后，他们的事务都可以更新数据库
     * 解决方案：将线程执行过程中，改变锁和事务的执行顺序
     * 旧：开启事务->获取锁->处理业务->释放锁->提交事务
     * 新：获取锁->开启事务->处理业务->提交事务->释放锁
     * 取消方法的调用者receiveCoupon()的事务，为checkAndCreateUserCoupon()添加事务
     * 锁主方法体checkAndCreateUserCoupon()
     */
    @Transactional
    public void checkAndCreateUserCoupon(Coupon coupon, Long userId, Integer serialNum) {
        // 1.校验每人限领数量
        // 1.1.统计当前用户对当前优惠券的已经领取的数量
        Integer count = lambdaQuery()
                .eq(UserCoupon::getUserId, userId)
                .eq(UserCoupon::getCouponId, coupon.getId())
                .count();
        // 1.2.校验限领数量
        if (count != null && count >= coupon.getUserLimit()) {
            throw new BadRequestException("超出领取数量");
        }
        // 2.更新优惠券的已经发放的数量 + 1
        int r = couponMapper.incrIssueNum(coupon.getId());
        if (r == 0) {
            throw new BizIllegalException("优惠券库存不足！");
        }
        // 3.新增一个用户券
        saveUserCoupon(coupon, userId);
        // 4.更新兑换码状态
        if (serialNum != null) {
            codeService.lambdaUpdate()
                    .set(ExchangeCode::getUserId, userId)
                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                    .eq(ExchangeCode::getId, serialNum)
                    .update();
        }
        // 异常必须被抛出，才能够回滚
//        throw new RuntimeException("测试");
    }

    private void saveUserCoupon(Coupon coupon, Long userId) {
        // 1.基本信息
        UserCoupon uc = new UserCoupon();
        uc.setUserId(userId);
        uc.setCouponId(coupon.getId());
        // 2.有效期信息
        LocalDateTime termBeginTime = coupon.getTermBeginTime();
        LocalDateTime termEndTime = coupon.getTermEndTime();
        if (termBeginTime == null) {
            termBeginTime = LocalDateTime.now();
            termEndTime = termBeginTime.plusDays(coupon.getTermDays());
        }
        uc.setTermBeginTime(termBeginTime);
        uc.setTermEndTime(termEndTime);
        // 3.保存
        save(uc);
    }
}
